using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Net;
using System.Xml;

namespace Horizon
{
    /// <summary>
    /// Methods directly related to server communications
    /// </summary>
    public class HorizonDataAccess
    {
        private Library _lib;

        public HorizonDataAccess()
        {
        }

        public HorizonDataAccess(Library lib)
        {
            _lib = lib;
        }

        /// <summary>
        /// Library associated with this object -- used for context such as URL
        /// </summary>
        public Library CurrentLibrary
        {
            get { return _lib; }
            set { _lib = value; }
        }

        /// <summary>
        /// Retrieve data remotely via an HTTP GET operation 
        /// </summary>
        /// <param name="req">Server request to execute</param>
        /// <param name="user">Patron for authenticated messages</param>
        /// <returns></returns>
        public XmlDocument ServerGET(ServerRequest req, Patron user)
        {
            // Retrieving the XML from the server can be done with one line of code,
            // but this method makes it easier to troubleshoot errors with malformed XML.
            WebClient wc = new WebClient();
            string data = wc.DownloadString(CreateRequestUrl(req, user));

            XmlDocument doc = new XmlDocument();
            doc.LoadXml(data);

            ErrorCheck(doc);

            // If no user is logged in, just update session key to match
            // If user should be logged in, make sure that session has not expired
            if (user == null) _lib.AnonSessionId = doc.SelectSingleNode("//session").InnerText;
            else if (doc.SelectSingleNode("//security/auth").InnerText == "false")
            {
                // Invalidate session, relogin, and try the request again.
                user.CurrentSession.Invalidate();
                _lib.Login(user);

                // Must regenerate URL due to new session ID
                doc.Load(CreateRequestUrl( req, user));
            }

            return doc;
        }

        /// <summary>
        /// Retrieves an XML response based on a POST operation (used for
        /// login and renew currently)
        /// </summary>
        /// <param name="postData">The data to POST to the library</param>
        /// <param name="user">Patron for authenticated messages</param>
        /// <returns></returns>
        public XmlDocument ServerPOST(string postData, Patron user)
        {
            XmlDocument doc = _ServerPOST(postData, user);

            ErrorCheck(doc);

            // If no user is set, just update session key to match
            // If user should be logged in, make sure that session has not expired
            if (user == null)
            {
                _lib.AnonSessionId = doc.SelectSingleNode("//session").InnerText;
            }
            else if (user.CurrentSession.LoggedIn && doc.SelectSingleNode("//security/auth").InnerText == "false")
            {
                // Invalidate session, relogin, and try the request again.
                user.CurrentSession.Invalidate();
                _lib.Login(user); // Note that this actually calls ServerPOST recursively(!)

                // Must POST again with new session data
                doc = _ServerPOST(postData, user);
            }

            return doc;
        }

        #region "Private utility functions"

        /// <summary>
        /// public POST logic.  Implemented separate so ServerPOST() can
        /// re-execute in case session timed out.
        /// </summary>
        /// <param name="postData">The data to POST to the library</param>
        /// <param name="user">Patron for authenticated messages</param>
        /// <returns></returns>
        private  XmlDocument _ServerPOST(string postData, Patron user)
        {
            // Create a POST request with the persistent cookie collection
            HttpWebRequest webRequest;
            webRequest = WebRequest.Create(_lib.BaseUrl) as HttpWebRequest;
            webRequest.Method = "POST";
            webRequest.ContentType = "application/x-www-form-urlencoded";
            webRequest.CookieContainer = user.CurrentSession.Cookies;

            // Write the form values into the request message
            StreamWriter requestWriter = new StreamWriter(webRequest.GetRequestStream());
            requestWriter.Write(postData);
            requestWriter.Close();

            // Read the response and close the stream
            StreamReader responseReader = new StreamReader(webRequest.GetResponse().GetResponseStream());
            string responseData = responseReader.ReadToEnd();
            responseReader.Close();

            // Load the response into an XML document
            XmlDocument doc = new XmlDocument();
            doc.LoadXml(responseData);

            return doc;
        }

        /// <summary>
        /// Overload to create a request URL for an anonymous user
        /// </summary>
        /// <param name="req">Request to be invoked</param>
        /// <returns>Full URL including request string</returns>
        public string CreateRequestUrl(ServerRequest req)
        {
            return CreateRequestUrl(req, null);
        }

        /// <summary>
        /// Creates a URL string to invoke a given request
        /// </summary>
        /// <param name="req">Request to be invoked</param>
        /// <param name="user">User on whose behalf request will be invoked</param>
        /// <returns>Full URL including request string</returns>
        public string CreateRequestUrl(ServerRequest req, Patron user)
        {
            StringBuilder fullUrl = new StringBuilder();

            fullUrl.Append(_lib.BaseUrl);

            if (_lib.BaseUrl.IndexOf('?') == -1) fullUrl.Append("?");

            if (user != null && user.CurrentSession.SessionKey != null)
                fullUrl.Append("session=").Append(user.CurrentSession.SessionKey);

            fullUrl.Append(string.Format(
                "&menu={0}&submenu={1}&aspect={2}&npp={3}&ipp={4}&spp={5}&profile={6}&ri={7}",
                req.Menu, req.Submenu, req.Aspect,
                req.NumberPerPage, req.IPerPage, req.SPerPage,
                _lib.Profile, req.RI
                ));

            // This can be repeated if an operation is in-between each (i.e. oper=and)
            fullUrl.Append(string.Format(
                "&index={0}&term={1}", // Match option would go here (i.e. matchopt=0%7C0)
                req.Index, req.SearchTerm
            ));

            // This limits by collection like Juvenile or Downtown Branch
            if (req.Limit != "")
                fullUrl.Append("&limitbox_1=").Append(req.Limit);

            // This sorts the results server-side like by author or title
            if (req.Sort != "")
                fullUrl.Append("&sort=").Append(req.Sort);

            // Without this we just get HTML!
            fullUrl.Append("&GetXML=true");

            return fullUrl.ToString();
        }

        /// <summary>
        /// Returns the InnerText property of a node if the node is not null.
        /// Convenience method to reduce use elsewhere.
        /// </summary>
        /// <param name="node">The node to examine</param>
        /// <returns>InnerText property if the node is non-null, else empty string</returns>
        public static string SafeInnerText(XmlNode node)
        {
            if (node != null) return node.InnerText;
            else return string.Empty;
        }

        /// <summary>
        /// Overload to extract node before checking InnerText
        /// </summary>
        /// <param name="doc"></param>
        /// <param name="nodeName"></param>
        /// <returns></returns>
        public static string SafeInnerText(XmlNode doc, string nodeName)
        {
            return SafeInnerText(doc.SelectSingleNode(nodeName));
        }

        /// <summary>
        /// Check for error section and throw exception if error found
        /// </summary>
        /// <param name="doc">Document to check for errors</param>
        public void ErrorCheck(XmlDocument doc)
        {
            XmlNode errorNode = doc.SelectSingleNode("//error/message");
            if (errorNode != null && errorNode.InnerText != "")
            {
                throw new HorizonError(doc.SelectSingleNode("//error"));
            }
        }

        #endregion
    }
}
